<?php
/**
 * TimoPHP a Fast Simple Smart PHP FrameWork
 * Author: Tommy 863758705@qq.com
 * Link: http://www.TimoPHP.com/
 * Since: 2016
 */

namespace Timo\Core;


class Model extends Application
{
    /**
     * @var string 数据库名
     */
    protected $dbName = '';

    /**
     * @var string 数据表名前缀
     */
    protected $prefix = null;

    /**
     * @var string 数据表名
     */
    protected $tableName = '';

    /**
     * @var string 数据表的主键
     */
    protected $primaryKey = '';

    /**
     * @var array 数据表字段信息
     */
    protected $tableFields = [];

    /**
     * @var Db 数据库实例
     */
    protected $db = null;

    /**
     * @var bool 是否启用读写分离
     */
    protected $rw_separate;

    /**
     * @var int 获取模式
     */
    protected $fetchMode = \PDO::FETCH_ASSOC;

    /**
     * 构造函数
     *
     * @param string $dbType 数据库类型，如：mysql、postgres、sqlite、sql_server
     * @param string $dbName 数据库实例名称
     * @param array $options 实例化数据库相关参数
     */
    public function __construct($dbType = '', $dbName = '', $options = array())
    {
        $dbType = !$dbType ? 'mysql' : $dbType;
        $dbName = !$dbName ? 'master' : $dbName;
        $this->connect($dbType, $dbName, $options);
    }

    /**
     * 连接数据库，获取数据库实例
     *
     * @param $dbType
     * @param $dbName
     * @param array $options
     */
    protected function connect($dbType, $dbName, $options = array())
    {
        $conf = Config::load('runtime')->get($dbType, $dbName);

        if (is_null($this->rw_separate)) {
            $this->rw_separate = !isset($conf['rw_separate']) ? false : $conf['rw_separate'];
        }
        $this->db = Db::getInstance($conf, $options, $this->rw_separate);
        $this->dbName = $dbName;
        $this->prefix = $conf['prefix'];
    }

    /**
     * 获取当前模型（Model）文件所对应的数据表前缀
     *
     * @access public
     * @return string
     */
    public function getTablePrefix()
    {
        return $this->prefix;
    }

    /**
     * 获取当前模型所对应的数据表名称（包括表前缀）
     *
     * @access public
     * @param string $table
     * @return string
     */
    public function getTableName($table = '')
    {
        if (!empty($table)) {
            return (!$this->prefix) ? $table : $this->prefix . $table;
        }
        if (!$this->tableName) {

            $tableName = lcfirst(substr(get_class($this), strrpos(get_class($this), '\\') + 1, -5));
            $tableName = preg_replace_callback('/[A-Z]?/', function ($match) {
                return !empty($match[0]) ? '_' . strtolower($match[0]) : '';
            }, $tableName);

            //当有前缀时，加上前缀
            $this->tableName = (!$this->prefix) ? $tableName : $this->prefix . $tableName;
        }

        return $this->tableName;
    }

    /**
     * 获取当前模型（Model）文件所对应的数据表主键
     *
     * 注：数据表的物理主键，真实存在的，不是虚拟的。
     *
     * @access protected
     * @return string
     */
    protected function getPrimaryKey()
    {
        if (!$this->primaryKey) {
            $tableName = $this->getTableName();
            //从缓存文件中读取
            if (!$this->loadCache($tableName)) {
                $this->createCache($tableName);
            }
        }

        return $this->primaryKey;
    }

    /**
     * 获取当前模型（Model）文件所对应的数据表字段信息
     *
     * @access protected
     * @return array
     */
    protected function getTableFields()
    {
        if (!$this->tableFields) {
            $tableName = $this->getTableName();
            //当回调方法未获取到数据表字段信息时，则从缓存文件中读取
            if (!$this->loadCache($tableName)) {
                $this->createCache($tableName);
            }
        }

        return $this->tableFields;
    }

    /**
     * 设置表前缀
     *
     * @param $prefix
     */
    public function setTablePrefix($prefix)
    {
        $this->prefix = $prefix;
    }

    /**
     * 设置当前模型（Model）文件所对应的数据表的名称（不包含表前缀）
     *
     * @param $tableName
     * @return $this
     */
    public function setTableName($tableName)
    {
        $tableName = trim($tableName);
        $this->tableName = (!$this->prefix) ? $tableName : $this->prefix . $tableName;
        return $this;
    }

    /**
     * 加载当前模型（Model）文件的缓存文件内容
     *
     * 注：缓存文件内容为：当前模型（Model）文件所对应的数据表的字段信息及主键信息。
     * @param $tableName
     * @return bool
     */
    protected function loadCache($tableName)
    {
        if (!$tableName) {
            return false;
        }

        $cacheFile = $this->getCacheFile($tableName);

        if (!is_file($cacheFile)) {
            return false;
        }

        $cache = include $cacheFile;

        $this->primaryKey = $cache['primaryKey'];
        $this->tableFields = $cache['fields'];

        //清空不必要的内存占用
        unset($cache);

        return true;
    }

    /**
     * 创建当前模型（Model）文件的缓存文件
     *
     * 注：缓存文件包含当前模型（Model）文件所对应的数据表的字段和主键信息，用于减轻数据反复查询数据表字段信息的操作，从而提高程序的运行效率。
     *
     * @access protected
     *
     * @param string $tableName 数据表名称
     *
     * @return bool
     */
    protected function createCache($tableName)
    {
        //参数分析
        if (!$tableName) {
            return false;
        }

        //获取数据表字段信息
        $tableInfo = $this->db->getTableInfo($tableName);

        $this->primaryKey = $tableInfo['primaryKey'][0];
        $this->tableFields = $tableInfo['fields'];

        if (APP_DEBUG) {
            return true;
        }

        //分析缓存文件内容
        $cacheDataArray = array(
            'primaryKey' => $this->primaryKey,
            'fields' => $this->tableFields,
        );

        $cacheContent = "<?php\nreturn " . var_export($cacheDataArray, true) . ";";

        //分析缓存文件路径
        $cacheFile = $this->getCacheFile($tableName);

        //分析缓存目录
        $cacheDir = dirname($cacheFile);
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0777, true);
        }

        //将缓存内容写入缓存文件
        file_put_contents($cacheFile, $cacheContent, LOCK_EX);

        return true;
    }

    /**
     * 删除当前模型（Model）文件的缓存文件
     *
     * 注：如果自定了数据表的字段信息及主键信息，则当前模型（Model）文件的缓存文件不再被使用。
     *
     * @access public
     *
     * @param string $tableName 数据表名
     *
     * @return boolean
     */
    public function removeCache($tableName)
    {
        //参数分析
        if (!$tableName) {
            return false;
        }

        //分析缓存文件路径
        $cacheFile = $this->getCacheFile($tableName);

        //当文件存在时，删除文件
        if (!is_file($cacheFile)) {
            return true;
        }

        return unlink($cacheFile);
    }

    /**
     * 获取数据表写入时的最新的Insert Id
     *
     * @access public
     * @return integer
     */
    public function lastInsertId()
    {
        return $this->db->lastInsertId();
    }

    /**
     * 获取PDOStatement
     *
     * @return $this
     */
    public function stmt()
    {
        $this->db->return_stmt = true;
        return $this;
    }

    /**
     * 更改获取模式
     *
     * @param $fetchMode
     * @return $this
     */
    public function fetchMode($fetchMode)
    {
        $this->fetchMode = $fetchMode;
        return $this;
    }

    /**
     * 事务处理：开启事务处理
     *
     * @access public
     * @return boolean
     */
    public function startTrans()
    {
        return $this->db->startTrans();
    }

    /**
     * 事务处理：提交事务处理
     *
     * @access public
     * @return boolean
     */
    public function commit()
    {
        return $this->db->commit();
    }

    /**
     * 事务处理：事务回滚
     *
     * @access public
     * @return boolean
     */
    public function rollback()
    {
        return $this->db->rollback();
    }

    /**
     * 执行查询SQL语句
     *
     * 用于执行查询性的SQL语句（需要数据返回的情况）
     *
     * @access public
     *
     * @param string $sql 所要执行的SQL语句
     * @param array $params 待转义的数据。注：本参数支持字符串及数组，如果待转义的数据量在两个或两个以上请使用数组
     *
     * @return Db
     */
    public function query($sql, array $params = null)
    {
        $sql = str_replace('__TABLE__', $this->getTableName(), $sql);
        return $this->db->query($sql, $params);
    }

    /**
     * 执行非查询SQL语句
     *
     * 用于无需返回信息的操作。如：更改、删除、添加数据
     *
     * @access public
     *
     * @param string $sql 所要执行的SQL语句
     * @param array $params 待转义的数据。注：本参数支持字符串及数组，如果待转义的数据量在两个或两个以上请使用数组
     *
     * @return boolean
     */
    public function execute($sql, $params = null)
    {
        if (!$sql) {
            return false;
        }

        //转义数据表前缀、表名
        $sql = str_replace('__TABLE__', $this->getTableName(), $sql);

        return $this->db->execute($sql, $params);
    }

    /**
     * 添加数据
     *
     * @param array $data
     * @param boolean $return_id 是否返回last_insert_id
     * @param string $table 数据表名称（包括表前缀）
     *
     * @return mixed
     */
    public function insert(array $data, $return_id = true, $table = '')
    {
        if (empty($table)) {
            $table = $this->getTableName();
            //数据过滤
            $data = $this->filterFields($data);
        }
        if (!$data) {
            return false;
        }

        return $this->db->insert($table, $data, $return_id);
    }

    /**
     * 批量插入数据
     *
     * @param array $data
     * @param bool $return_id
     * @param string $table
     * @return bool|int
     */
    public function insertMulti(array $data, $return_id = true, $table = '')
    {
        if (!is_array($data[0])) {
            return false;
        }
        if (empty($table)) {
            $table = $this->getTableName();
        }

        return $this->db->insertMulti($table, $data, $return_id);
    }

    /**
     * 删除数据
     *
     * @param int|string|array $where
     * @param string $table
     * @return bool
     */
    public function delete($where, $table = '')
    {
        if (empty($table)) {
            $table = $this->getTableName();
        }

        $params = [];
        $where = $this->parseWhere($where, $params);

        return $this->db->delete($table, $where, $params);
    }

    /**
     * 更新数据
     *
     * @param string|array $data
     * @param int|string|array $where
     * @param string $table
     * @return bool
     */
    public function update($data, $where, $table = '')
    {
        if (empty($table)) {
            $table = $this->getTableName();
            $data = $this->filterFields($data);
        }
        if (!$data) {
            return false;
        }

        $params = [];
        $where = $this->parseWhere($where, $params);

        return $this->db->update($table, $data, $where, $params);
    }

    /**
     * 通过主键获取记录
     *
     * @param int|array $id 主键ID
     * @param string $fields
     * @param string $table
     * @return array|bool
     */
    public function get($id, $fields = '*', $table = '')
    {
        if (empty($table)) {
            $table = $this->getTableName();
        }

        //主键
        $primaryKey = $this->getPrimaryKey();

        $sql = 'SELECT ' . $fields . ' FROM ' . $table . ' WHERE ' . $primaryKey;

        if (is_array($id)) {
            $id = array_map('intval', $id);
            $sql .= " IN (" . implode(',', $id) . ")";
            $row = $this->db->getAll($sql);
        } else {
            $sql .= " = " . $this->db->escape($id);
            $row = $this->db->getOne($sql);
        }
        return $row;
    }

    /**
     * 获取一条记录
     *
     * @param string|array $where
     * @param string $fields
     * @param string $order
     * @param string $group
     * @param string $table
     * @return array|\PDOStatement
     */
    public function getRow($where, $fields = '*', $order = '', $group = '', $table = '')
    {
        if (empty($table)) {
            $table = $this->getTableName();
        }

        $params = [];
        $condition = $this->parseWhere($where, $params);

        $sql = 'SELECT ' . $fields . ' FROM ' . $table . ' WHERE ' . $condition;

        if (!empty($group)) {
            $sql .= ' GROUP BY ' . $group;
        }
        if (!empty($order)) {
            $sql .= ' ORDER BY ' . $order;
        }
        $sql .= ' LIMIT 1';
        return $this->db->getOne($sql, $params);
    }

    /**
     * 获取多条数据
     *
     * @param string|array $where
     * @param string $fields
     * @param string $order
     * @param string $group
     * @param int $limit
     * @param string $table
     * @return array|bool|\PDOStatement
     */
    public function getAll($where, $fields = '*', $order = '', $group = '', $limit = 0, $table = '')
    {
        $fetch_mode = $this->fetchMode;
        $this->fetchMode = \PDO::FETCH_ASSOC;

        if (empty($table)) {
            $table = $this->getTableName();
        }
        $params = [];
        $condition = $this->parseWhere($where, $params);

        $sql = 'SELECT ' . $fields . ' FROM ' . $table . ' WHERE ' . $condition;

        if (!empty($group)) {
            $sql .= ' GROUP BY ' . $group;
        }
        if (!empty($order)) {
            $sql .= ' ORDER BY ' . $order;
        }
        if ($limit > 0) {
            $sql .= ' LIMIT ' . $limit;
        }
        return $this->db->getAll($sql, $params, $fetch_mode);
    }

    /**
     * 分页获取多条数据
     *
     * @param string|array $where
     * @param string $fields
     * @param string $order
     * @param string $group
     * @param array $page
     * @param string $table
     * @return array|bool
     */
    public function find($where, $fields = '*', $order = '', $group = '', array &$page = array('p' => 1, 'limit' => 20), $table = '')
    {
        $fetch_mode = $this->fetchMode;
        $this->fetchMode = \PDO::FETCH_ASSOC;

        if (empty($table)) {
            $table = $this->getTableName();
        }

        $total = $this->getRow($where, 'COUNT(*) as total');
        $page['total'] = (int)$total['total'];
        $page['total_page'] = ceil($page['total'] / $page['limit']);

        $params = [];
        $condition = $this->parseWhere($where, $params);

        $sql = 'SELECT ' . $fields . ' FROM ' . $table . ' WHERE ' . $condition;

        if (!empty($group)) {
            $sql .= ' GROUP BY ' . $group;
        }
        if (!empty($order)) {
            $sql .= ' ORDER BY ' . $order;
        }

        if ($page['p'] <= $page['total_page']) {
            $sql .= ' LIMIT ' . ($page['p'] - 1) * $page['limit'] . ',' . $page['limit'];
            $row = $this->db->getAll($sql, $params, $fetch_mode);
        } else {
            $row = array();
        }
        return $row;
    }

    /**
     * 统计条数
     *
     * @param string|array $where
     * @return int
     */
    public function getCount($where)
    {
        $total = $this->getRow($where, 'COUNT(*) as total');
        return (int)$total['total'];
    }

    /**
     * 分析当前model缓存文件的路径
     *
     * @param string $tableName 数据表名
     * @return string 缓存文件的路径
     */
    protected function getCacheFile($tableName)
    {
        return CACHE_PATH . 'models' . DS . $tableName . '.cache.php';
    }

    /**
     * 过虑数据表字段信息
     *
     * 用于insert()、update()里的字段信息进行过虑，删除掉非法的字段信息。
     * @param array $data
     * @return array
     */
    protected function filterFields(array $data = [])
    {
        //获取数据表字段
        $tableFields = $this->getTableFields();

        $filteredArray = [];
        foreach ($data as $key => $value) {
            if (in_array($key, $tableFields)) {
                $filteredArray[$key] = $value;
            }
        }

        return $filteredArray;
    }

    /**
     * 解析where
     *
     * @param array|string $where
     * @param array|null $params
     * @return string
     */
    private function parseWhere($where, &$params)
    {
        if (is_string($where)) {
            return $where;
        }

        if (is_int($where)) {
            $pk = $this->getPrimaryKey();
            $where = [$pk => $where];
        }

        $condition = '';

        foreach ($where as $key => $value) {
            if (!is_array($value)) {
                $condition .= '`' . $key . '`' . '=? AND ';
                array_push($params, $value);
            } else {
                switch ($value[0]) {
                    case '<':
                    case '>':
                    case '<>':
                    case 'gt':
                    case 'lt':
                    case 'like':
                        $condition .= '`' . $key . '` ' . $value[0] . ' ? AND ';
                        array_push($params, $value[1]);
                        break;
                    case 'between':
                        $condition .= '`' . $key . '` ' . $value[0] . ' ? AND ? AND ';
                        array_push($params, $value[1]);
                        array_push($params, $value[2]);
                        break;
                    case 'in':
                        $condition .= '`' . $key . '` ' . ' IN(?) AND ';
                        array_push($params, $value[1]);
                        break;
                }
            }
        }
        return trim($condition, 'AND ');
    }
}
